import * as THREE from 'three'

const PINCH_MAX = 0.05
const PINCH_THRESHOLD = 0.02
const PINCH_MIN = 0.01
const POINTER_ADVANCE_MAX = 0.02
const POINTER_OPACITY_MAX = 1
const POINTER_OPACITY_MIN = 0.4
const POINTER_FRONT_RADIUS = 0.002
const POINTER_REAR_RADIUS = 0.01
const POINTER_REAR_RADIUS_MIN = 0.003
const POINTER_LENGTH = 0.035
const POINTER_SEGMENTS = 16
const POINTER_RINGS = 12
const POINTER_HEMISPHERE_ANGLE = 110
const YAXIS = /* @__PURE__ */ new THREE.Vector3(0, 1, 0)
const ZAXIS = /* @__PURE__ */ new THREE.Vector3(0, 0, 1)

const CURSOR_RADIUS = 0.02
const CURSOR_MAX_DISTANCE = 1.5

class OculusHandPointerModel extends THREE.Object3D {
  constructor(hand, controller) {
    super()

    this.hand = hand
    this.controller = controller

    // Unused
    this.motionController = null
    this.envMap = null
    this.mesh = null

    this.pointerGeometry = null
    this.pointerMesh = null
    this.pointerObject = null

    this.pinched = false
    this.attached = false

    this.cursorObject = null

    this.raycaster = null

    this._onConnected = this._onConnected.bind(this)
    this._onDisconnected = this._onDisconnected.bind(this)
    this.hand.addEventListener('connected', this._onConnected)
    this.hand.addEventListener('disconnected', this._onDisconnected)
  }

  _onConnected(event) {
    const xrInputSource = event.data
    if (xrInputSource.hand) {
      this.visible = true
      this.xrInputSource = xrInputSource

      this.createPointer()
    }
  }

  _onDisconnected() {
    this.visible = false
    this.xrInputSource = null

    this.pointerGeometry?.dispose()
    this.pointerMesh?.material.dispose()

    this.clear()
  }

  _drawVerticesRing(vertices, baseVector, ringIndex) {
    const segmentVector = baseVector.clone()
    for (var i = 0; i < POINTER_SEGMENTS; i++) {
      segmentVector.applyAxisAngle(ZAXIS, (Math.PI * 2) / POINTER_SEGMENTS)
      const vid = ringIndex * POINTER_SEGMENTS + i
      vertices[3 * vid] = segmentVector.x
      vertices[3 * vid + 1] = segmentVector.y
      vertices[3 * vid + 2] = segmentVector.z
    }
  }

  _updatePointerVertices(rearRadius) {
    const vertices = this.pointerGeometry.attributes.position.array
    // first ring for front face
    const frontFaceBase = new THREE.Vector3(POINTER_FRONT_RADIUS, 0, -1 * (POINTER_LENGTH - rearRadius))
    this._drawVerticesRing(vertices, frontFaceBase, 0)

    // rings for rear hemisphere
    const rearBase = new THREE.Vector3(
      Math.sin((Math.PI * POINTER_HEMISPHERE_ANGLE) / 180) * rearRadius,
      Math.cos((Math.PI * POINTER_HEMISPHERE_ANGLE) / 180) * rearRadius,
      0,
    )
    for (var i = 0; i < POINTER_RINGS; i++) {
      this._drawVerticesRing(vertices, rearBase, i + 1)
      rearBase.applyAxisAngle(YAXIS, (Math.PI * POINTER_HEMISPHERE_ANGLE) / 180 / (POINTER_RINGS * -2))
    }

    // front and rear face center vertices
    const frontCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS)
    const rearCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS) + 1
    const frontCenter = new THREE.Vector3(0, 0, -1 * (POINTER_LENGTH - rearRadius))
    vertices[frontCenterIndex * 3] = frontCenter.x
    vertices[frontCenterIndex * 3 + 1] = frontCenter.y
    vertices[frontCenterIndex * 3 + 2] = frontCenter.z
    const rearCenter = new THREE.Vector3(0, 0, rearRadius)
    vertices[rearCenterIndex * 3] = rearCenter.x
    vertices[rearCenterIndex * 3 + 1] = rearCenter.y
    vertices[rearCenterIndex * 3 + 2] = rearCenter.z

    this.pointerGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))
    // verticesNeedUpdate = true;
  }

  createPointer() {
    var i, j
    const vertices = new Array(((POINTER_RINGS + 1) * POINTER_SEGMENTS + 2) * 3).fill(0)
    // const vertices = [];
    const indices = []
    this.pointerGeometry = new THREE.BufferGeometry()

    this.pointerGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3))

    this._updatePointerVertices(POINTER_REAR_RADIUS)

    // construct faces to connect rings
    for (i = 0; i < POINTER_RINGS; i++) {
      for (j = 0; j < POINTER_SEGMENTS - 1; j++) {
        indices.push(i * POINTER_SEGMENTS + j, i * POINTER_SEGMENTS + j + 1, (i + 1) * POINTER_SEGMENTS + j)
        indices.push(i * POINTER_SEGMENTS + j + 1, (i + 1) * POINTER_SEGMENTS + j + 1, (i + 1) * POINTER_SEGMENTS + j)
      }

      indices.push((i + 1) * POINTER_SEGMENTS - 1, i * POINTER_SEGMENTS, (i + 2) * POINTER_SEGMENTS - 1)
      indices.push(i * POINTER_SEGMENTS, (i + 1) * POINTER_SEGMENTS, (i + 2) * POINTER_SEGMENTS - 1)
    }

    // construct front and rear face
    const frontCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS)
    const rearCenterIndex = POINTER_SEGMENTS * (1 + POINTER_RINGS) + 1

    for (i = 0; i < POINTER_SEGMENTS - 1; i++) {
      indices.push(frontCenterIndex, i + 1, i)
      indices.push(rearCenterIndex, i + POINTER_SEGMENTS * POINTER_RINGS, i + POINTER_SEGMENTS * POINTER_RINGS + 1)
    }

    indices.push(frontCenterIndex, 0, POINTER_SEGMENTS - 1)
    indices.push(rearCenterIndex, POINTER_SEGMENTS * (POINTER_RINGS + 1) - 1, POINTER_SEGMENTS * POINTER_RINGS)

    const material = new THREE.MeshBasicMaterial()
    material.transparent = true
    material.opacity = POINTER_OPACITY_MIN

    this.pointerGeometry.setIndex(indices)

    this.pointerMesh = new THREE.Mesh(this.pointerGeometry, material)

    this.pointerMesh.position.set(0, 0, -1 * POINTER_REAR_RADIUS)
    this.pointerObject = new THREE.Object3D()
    this.pointerObject.add(this.pointerMesh)

    this.raycaster = new THREE.Raycaster()

    // create cursor
    const cursorGeometry = new THREE.SphereGeometry(CURSOR_RADIUS, 10, 10)
    const cursorMaterial = new THREE.MeshBasicMaterial()
    cursorMaterial.transparent = true
    cursorMaterial.opacity = POINTER_OPACITY_MIN

    this.cursorObject = new THREE.Mesh(cursorGeometry, cursorMaterial)
    this.pointerObject.add(this.cursorObject)

    this.add(this.pointerObject)
  }

  _updateRaycaster() {
    if (this.raycaster) {
      const pointerMatrix = this.pointerObject.matrixWorld
      const tempMatrix = new THREE.Matrix4()
      tempMatrix.identity().extractRotation(pointerMatrix)
      this.raycaster.ray.origin.setFromMatrixPosition(pointerMatrix)
      this.raycaster.ray.direction.set(0, 0, -1).applyMatrix4(tempMatrix)
    }
  }

  _updatePointer() {
    this.pointerObject.visible = this.controller.visible
    const indexTip = this.hand.joints['index-finger-tip']
    const thumbTip = this.hand.joints['thumb-tip']
    const distance = indexTip.position.distanceTo(thumbTip.position)
    const position = indexTip.position.clone().add(thumbTip.position).multiplyScalar(0.5)
    this.pointerObject.position.copy(position)
    this.pointerObject.quaternion.copy(this.controller.quaternion)

    this.pinched = distance <= PINCH_THRESHOLD

    const pinchScale = (distance - PINCH_MIN) / (PINCH_MAX - PINCH_MIN)
    const focusScale = (distance - PINCH_MIN) / (PINCH_THRESHOLD - PINCH_MIN)
    if (pinchScale > 1) {
      this._updatePointerVertices(POINTER_REAR_RADIUS)
      this.pointerMesh.position.set(0, 0, -1 * POINTER_REAR_RADIUS)
      this.pointerMesh.material.opacity = POINTER_OPACITY_MIN
    } else if (pinchScale > 0) {
      const rearRadius = (POINTER_REAR_RADIUS - POINTER_REAR_RADIUS_MIN) * pinchScale + POINTER_REAR_RADIUS_MIN
      this._updatePointerVertices(rearRadius)
      if (focusScale < 1) {
        this.pointerMesh.position.set(0, 0, -1 * rearRadius - (1 - focusScale) * POINTER_ADVANCE_MAX)
        this.pointerMesh.material.opacity =
          POINTER_OPACITY_MIN + (1 - focusScale) * (POINTER_OPACITY_MAX - POINTER_OPACITY_MIN)
      } else {
        this.pointerMesh.position.set(0, 0, -1 * rearRadius)
        this.pointerMesh.material.opacity = POINTER_OPACITY_MIN
      }
    } else {
      this._updatePointerVertices(POINTER_REAR_RADIUS_MIN)
      this.pointerMesh.position.set(0, 0, -1 * POINTER_REAR_RADIUS_MIN - POINTER_ADVANCE_MAX)
      this.pointerMesh.material.opacity = POINTER_OPACITY_MAX
    }

    this.cursorObject.material.opacity = this.pointerMesh.material.opacity
  }

  updateMatrixWorld(force) {
    super.updateMatrixWorld(force)
    if (this.pointerGeometry) {
      this._updatePointer()
      this._updateRaycaster()
    }
  }

  isPinched() {
    return this.pinched
  }

  setAttached(attached) {
    this.attached = attached
  }

  isAttached() {
    return this.attached
  }

  intersectObject(object, recursive = true) {
    if (this.raycaster) {
      return this.raycaster.intersectObject(object, recursive)
    }
  }

  intersectObjects(objects, recursive = true) {
    if (this.raycaster) {
      return this.raycaster.intersectObjects(objects, recursive)
    }
  }

  checkIntersections(objects, recursive = false) {
    if (this.raycaster && !this.attached) {
      const intersections = this.raycaster.intersectObjects(objects, recursive)
      const direction = new THREE.Vector3(0, 0, -1)
      if (intersections.length > 0) {
        const intersection = intersections[0]
        const distance = intersection.distance
        this.cursorObject.position.copy(direction.multiplyScalar(distance))
      } else {
        this.cursorObject.position.copy(direction.multiplyScalar(CURSOR_MAX_DISTANCE))
      }
    }
  }

  setCursor(distance) {
    const direction = new THREE.Vector3(0, 0, -1)
    if (this.raycaster && !this.attached) {
      this.cursorObject.position.copy(direction.multiplyScalar(distance))
    }
  }

  dispose() {
    this._onDisconnected()
    this.hand.removeEventListener('connected', this._onConnected)
    this.hand.removeEventListener('disconnected', this._onDisconnected)
  }
}

export { OculusHandPointerModel }
